import { aliases } from "@/aliases.ts"
import { externals } from "@/externals.ts"
import { BuildOptions, resolveEntries } from "@/options.ts"
import { cleanDir } from "@/utils.ts"
import chalk from "chalk"
import { consola } from "consola"
import { existsSync, statSync } from "node:fs"
import { resolve } from "node:path"
import {
  defineConfig,
  OutputOptions,
  rolldown,
  RolldownOptions,
  RolldownPlugin,
} from "rolldown"
import { dts } from "rolldown-plugin-dts"

export * from "@/aliases.ts"
export * from "@/externals.ts"
export * from "@/options.ts"

/**
 * 生成 rolldown 配置选项。
 *
 * @param options 构建选项。
 * @param log 是否需要在命令行输出信息。
 * @returns rolldown 配置选项数组。
 */
export function generateRolldownOptions(options?: BuildOptions, log = false) {
  const {
    root = "",
    src = "src",
    out = "out",
    bin = "src/main.ts",
    lib = "src/index.ts",
    specials,
    cleanOutdir = true,
    tsconfig: t,
    external: e,
  } = options || {}

  // Common data.
  const resolvedRoot = resolve(root)
  const builds: RolldownOptions[] = []
  const plugins: RolldownPlugin[] = [aliases(t), externals({ root, ...e })]
  const common = defineConfig({
    plugins,
    output: {
      dir: out,
      format: "esm",
      minify: true,
      sourcemap: true,
      assetFileNames: "assets/[hash].[ext]",
      chunkFileNames: "chunks/[hash].js",
      entryFileNames: "[name].js",
    },
  })

  // Libs and bins.
  const bins = resolveEntries(src, bin)
  const libs = resolveEntries(src, lib)
  if (Object.keys(bins).length > 0) {
    if (log) {
      consola.log(``)
      consola.info(`需打包如下可执行程序${chalk.dim(`: ${bin}`)}`)
      for (const [name, path] of Object.entries(bins)) {
        consola.log(`  ${chalk.yellow(name)}${chalk.dim(`: ${path}`)}`)
      }
    }
    builds.push({ ...common, input: bins })
  }
  if (Object.keys(libs).length > 0) {
    if (log) {
      consola.log(``)
      consola.info(`需打包如下输出库${chalk.dim(`: ${lib}`)}`)
      for (const [name, path] of Object.entries(libs)) {
        consola.log(`  ${chalk.blue(name)}${chalk.dim(`: ${path}`)}`)
      }
    }
    const esm = defineConfig({ ...common, input: libs })
    const cJS: OutputOptions = { format: "cjs", entryFileNames: "[name].cjs" }
    const withDts = [...plugins, dts({ sourcemap: true })]
    builds.push({ ...esm, plugins: withDts, input: libs })
    builds.push({ ...esm, output: { ...esm.output, ...cJS } })
  }
  if (log) consola.log("")

  /** @yields 当前解析的特殊选项。 */
  function* specialOptions(): Generator<RolldownOptions> {
    for (const [name, value] of Object.entries(specials || {})) {
      const { input, outdir, cleanOutdir } = value
      if (existsSync(input) && statSync(input).isFile()) {
        let shouldCleanOutdir = false
        const inside = resolve(outdir).startsWith(resolvedRoot)

        // Resolve cleanOutdir.
        if (cleanOutdir) {
          if (inside || cleanOutdir === "force") shouldCleanOutdir = true
          if (!inside) {
            consola.warn(
              `cannot clean outdir(${chalk.dim(outdir)})` +
                `outside root(${chalk.dim(root)}) ` +
                `in special output from ${chalk.dim(input)}:` +
                chalk.dim(`you can set ${chalk.yellow("cleanOutdir")} of`) +
                chalk.dim(`this special build to ${chalk.yellow('"force"')}`) +
                chalk.dim(`to enable clean outdir regardless where it is.`),
            )
          }
        }

        yield defineConfig({
          plugins,
          input: { [name]: input },
          output: {
            ...common.output,
            dir: resolve(root, outdir),
            cleanDir: shouldCleanOutdir,
          },
        })
      }
    }
  }

  // Resolve cleanOutdir.
  if (cleanOutdir) {
    const inside = resolve(out).startsWith(resolvedRoot)
    if (inside || cleanOutdir === "force") cleanDir(out)
    if (!inside) {
      consola.warn(
        `cannot clean outdir(${chalk.dim(out)}) ` +
          `outside root(${chalk.dim(root)}): ` +
          chalk.dim(`you can set ${chalk.yellow("cleanOutdir")} `) +
          chalk.dim(`to ${chalk.yellow('"force"')}`) +
          chalk.dim(`to enable clean outdir regardless where it is.`),
      )
    }
  }

  return [...builds, ...specialOptions()]
}

/**
 * 根据提供的或默认的 {@link BuildOptions} 进行构建，
 * 由 {@link generateRolldownOptions} 预处理，
 * 基于 Rolldown 实现。
 *
 * @param options 配置如何构建。
 * @param log 是否需要在命令行输出信息。
 */
export async function build(options?: BuildOptions, log = false) {
  async function rolldownBuild(options: RolldownOptions) {
    const bundler = await rolldown(options)
    const output = options.output
    if (!output) return
    return Array.isArray(output)
      ? Promise.all(output.map(bundler.write))
      : bundler.write(output)
  }

  if (log) consola.start(`正在打包...`)
  await Promise.all(generateRolldownOptions(options, log).map(rolldownBuild))
  if (log) consola.success(`打包完成！`)
}
